package io.mycat.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * used for mycat's catlet class loader ,catlet's class file is stored in Mycat_home/catlet dir
 *
 * @author wuzhih
 */
public class DynaClassLoader {

  private static final Logger LOGGER = LoggerFactory.getLogger(DynaClassLoader.class);
  /**
   * key- class full name
   */
  private static Map<String, DynaClass> loadedDynaClassMap = new ConcurrentHashMap<String, DynaClass>();
  private final String extClassHome;
  private final MyDynaClassLoader myClassLoader;
  private final long classCheckMilis;

  public DynaClassLoader(String extClassHome, int classCheckSeconds) {
    super();
    this.extClassHome = extClassHome;
    classCheckMilis = classCheckSeconds * 1000L;
    myClassLoader = new MyDynaClassLoader();
    LOGGER.info("dyna class load from " + extClassHome
                    + ",and auto check for class file modified every "
                    + classCheckSeconds + " seconds");
  }

  public Object getInstanceofClass(String className) throws Exception {
    DynaClass dynaClass = loadedDynaClassMap.get(className);
    boolean needReload = (dynaClass == null || (dynaClass
                                                    .needReloadClass(classCheckMilis)
                                                    && checkChanged(dynaClass)));
    Class<?> newClass = null;
    if (needReload) {
      newClass = myClassLoader.loadClass(className);
      dynaClass = loadedDynaClassMap.get(className);
    } else {
      newClass = dynaClass.realClass;
    }

    if (dynaClass != null) {
      Object val = dynaClass.classObj;
      if (val == null) {
        val = dynaClass.realClass.newInstance();
        dynaClass.classObj = val;

      }
      return val;
    } else {
      return newClass.newInstance();
    }
  }

  /**
   * 加载某个类的字节码
   */
  private static byte[] loadFile(String path) throws IOException {
    BufferedInputStream in = null;
    try {
      in = new BufferedInputStream(new FileInputStream(path));
      byte[] readed = new byte[1024 * 4];
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      int count = 0;
      while ((count = in.read(readed)) != -1) {
        out.write(readed, 0, count);
      }
      return out.toByteArray();
    } finally {
      if (in != null) {
        in.close();
      }
    }
  }

  private boolean checkChanged(DynaClass dynaClass) throws IOException {
    boolean isChanged = false;
    File f = new File(dynaClass.filePath);
    if (f.exists()) {
      long newTime = f.lastModified();
      long oldTime = dynaClass.lastModified;
      if (oldTime != newTime) {
        // need reload
        dynaClass.lastModified = newTime;
        dynaClass.classObj = null;
        dynaClass.realClass = null;
        isChanged = true;
      }
    }
    return isChanged;
  }

  class MyDynaClassLoader extends ClassLoader {

    public MyDynaClassLoader() {
    }

    public MyDynaClassLoader(ClassLoader parentLoader) {
      super(parentLoader);
    }

    /**
     * 加载某个类
     */
    public Class<?> loadClass(String name) throws ClassNotFoundException {
      if (name.startsWith("java") || name.startsWith("sun")
              || name.startsWith("io.mycat")) {
        return super.loadClass(name);
      }
      DynaClass dynaClass = loadedDynaClassMap.get(name);
      if (dynaClass != null) {
        if (dynaClass.realClass != null) {
          return dynaClass.realClass;
        }
      } else {
        try {
          dynaClass = searchFile(extClassHome, name);
        } catch (Exception e) {
          LOGGER.error("SearchFileError", e);
        }
      }

      if (dynaClass == null) {
        return super.loadClass(name);
      } else {
        LOGGER.info("load class from file " + dynaClass.filePath);
        Class<?> cNew = null;
        if (dynaClass.isJar) {
          cNew = dynaClass.realClass;
        } else {
          byte[] content;
          try {
            content = loadFile(dynaClass.filePath);
          } catch (IOException e) {
            throw new ClassNotFoundException(e.toString());
          }
          cNew = super.defineClass(name, content, 0, content.length);
          dynaClass.realClass = cNew;
        }
        dynaClass.classObj = null;
        loadedDynaClassMap.put(name, dynaClass);
        return cNew;
      }

    }

    private DynaClass searchFile(String classpath, String fileName) throws Exception {
      DynaClass dynCls = null;
      String path = fileName.replace('.', File.separatorChar) + ".class";
      System.out.println("class " + classpath + " file " + path);
      File f = new File(classpath, path);
      if (f.isFile()) {
        String theName = f.getPath();
        System.out.println("found " + theName);

        dynCls = new DynaClass(f.getPath());
        dynCls.lastModified = f.lastModified();
        return dynCls;
      } else {
        path = fileName.replace('.', File.separatorChar) + ".jar";
        //classpath="D:\\code\\mycat\\Mycat-Server\\catlet\\";
        System.out.println("jar " + classpath + " file " + path);
        f = new File(classpath, path);
        if (f.isFile()) {
          try {
            dynCls = new DynaClass(f.getPath());
            dynCls.lastModified = f.lastModified();
            dynCls.realClass = JarLoader.loadJar(classpath + "/" + path, fileName);
            dynCls.isJar = true;
            return dynCls;
          } catch (Exception err) {
            return null;
          }

        }
        return null;
      }

    }

  }

  public void clearUnUsedClass() {
    long deadTime = System.currentTimeMillis() - 30 * 60 * 1000L;
    Iterator<Map.Entry<String, DynaClass>> itor = loadedDynaClassMap
                                                      .entrySet().iterator();
    while (itor.hasNext()) {
      Map.Entry<String, DynaClass> entry = itor.next();
      DynaClass dyCls = entry.getValue();
      if (dyCls.lastModified < deadTime) {
        LOGGER.info("reset unused catlet " + entry.getKey());
        dyCls.clear();
        itor.remove();
      }
    }
  }
}

class DynaClass {

  public final String filePath;
  public volatile long lastModified;
  public Class<?> realClass;
  public Object classObj;
  public boolean isJar = false;

  public boolean needReloadClass(long classCheckMilis) {
    return lastModified + classCheckMilis < System.currentTimeMillis();
  }

  public void clear() {
    this.realClass = null;
    this.classObj = null;

  }

  public DynaClass(String filePath) {
    super();
    this.filePath = filePath;
  }
}
